package org.dromara.common.web.filter;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HtmlUtil;
import org.dromara.common.core.utils.StringUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：XSS过滤处理
 */
/**
 * XssHttpServletRequestWrapper类是对HttpServletRequest的包装类，用于过滤XSS攻击（跨站脚本攻击）。
 * 它通过覆盖HttpServletRequest的getParameter方法以及相关的获取请求参数的方法，
 * 来实现对请求参数的过滤和清洗，从而防止XSS攻击，提高Web应用的安全性。
 */
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {

    /**
     * 构造函数：初始化XssHttpServletRequestWrapper对象
     * <p>
     * 该构造函数通过调用父类的构造函数，将传入的HttpServletRequest对象包装为XssHttpServletRequestWrapper对象
     * 这样做可以在访问请求中的参数和内容时自动进行XSS攻击的过滤和防护
     *
     * @param request 原始的HttpServletRequest对象，用于初始化XssHttpServletRequestWrapper对象
     */
    public XssHttpServletRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    /**
     * 根据参数名称获取参数的所有值
     *
     * @param name 参数名称
     * @return 参数的所有值组成的数组如果参数有多个值，返回数组包含多个值；
     *         如果参数没有值，返回null
     */
    @Override
    public String[] getParameterValues(String name) {
        // 调用父类方法获取请求参数的所有值
        String[] values = super.getParameterValues(name);
        // 检查是否获取到了参数值
        if (values != null) {
            // 获取参数值数组的长度
            int length = values.length;
            // 初始化一个同样长度的数组用于存放处理后的参数值
            String[] escapseValues = new String[length];
            // 遍历原始参数值数组
            for (int i = 0; i < length; i++) {
                // 防xss攻击和过滤前后空格
                // 对每个参数值进行HTML标签的清除和前后空格的修剪，以防止XSS攻击和规范参数格式
                escapseValues[i] = HtmlUtil.cleanHtmlTag(values[i]).trim();
            }
            // 返回处理后的参数值数组
            return escapseValues;
        }
        // 如果初始获取的参数值为null，调用父类方法返回默认值
        return super.getParameterValues(name);
    }

    /**
     * 获取与当前请求关联的输入流
     * 此方法用于读取客户端发送来的数据，例如表单提交或文件上传
     * 它是HttpServletRequest类的一部分，本实现特别地处理了如何获取输入流
     *
     * @return 返回一个ServletInputStream，可以用来读取请求体中的数据
     * @throws IOException 如果读取输入流时发生输入输出错误
     */
    @Override
    public ServletInputStream getInputStream() throws IOException {
        // 非json类型，直接返回
        // 对于非JSON类型的请求，直接返回父类的实现
        if (!isJsonRequest()) {
            return super.getInputStream();
        }

        // 为空，直接返回
        // 读取请求体并转换为字符串，使用UTF-8编码
        String json = StrUtil.str(IoUtil.readBytes(super.getInputStream(), false), StandardCharsets.UTF_8);
        // 如果JSON字符串为空，直接返回父类的实现
        if (StringUtils.isEmpty(json)) {
            return super.getInputStream();
        }

        // xss过滤 对JSON字符串进行XSS攻击过滤，移除HTML标签
        json = HtmlUtil.cleanHtmlTag(json).trim();
        // 将处理后的JSON字符串转换为字节流
        byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
        final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
        // 返回自定义的ServletInputStream实例
        return new ServletInputStream() {
            /**
             * 始终返回true，表示输入流已经结束
             */
            @Override
            public boolean isFinished() {
                return true;
            }

            /**
             * 始终返回true，表示输入流已准备好
             */
            @Override
            public boolean isReady() {
                return true;
            }

            /**
             * 返回可用字节的数量
             */
            @Override
            public int available() throws IOException {
                return jsonBytes.length;
            }

            /**
             * 空实现，因为在此自定义输入流中不需要设置读取监听器
             */
            @Override
            public void setReadListener(ReadListener readListener) {
            }

            /**
             * 从字节数组输入流中读取下一个字节
             */
            @Override
            public int read() throws IOException {
                return bis.read();
            }
        };
    }

    /**
     * 是否是Json请求
     * 判断请求是否为JSON格式
     *
     * 本方法通过检查请求头中的Content-Type字段来判断请求是否应被识别为JSON
     * 它首先获取Content-Type头，然后检查其值是否以"application/json"或者类似的JSON媒体类型开始
     * 这种检查是忽略大小写的，因此"application/json"、"Application.Json"等都会被认为是JSON请求
     *
     * @return boolean 返回一个布尔值，true表示请求是JSON格式的，false表示不是
     */
    public boolean isJsonRequest() {
        String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
        return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
    }
}
